Skip to content

fix(texlive-tags): tell a grep failure from zero matches - #175

Merged
toshi0806 merged 1 commit into
mainfrom
tell-grep-failures-from-zero-matches
Sep 6, 2026
Merged

fix(texlive-tags): tell a grep failure from zero matches#175
toshi0806 merged 1 commit into
mainfrom
tell-grep-failures-from-zero-matches

Conversation

@toshi0806

Copy link
Copy Markdown
Member

Resolves #173

監査が grep の標準エラーを捨てていたため、起動すらしなかった grep と、何も見つけなかった grep が同じ結果になっていました。前者でもそのリポジトリは ok と報告されます。

なぜ終了コードでは判定できないか

grep 1 = マッチなし(正常)
grep 2 = エラー(異常)
xargs  = 子プロセスが 1..125 で終わると一律 123 を返す

xargs が両者を同じ値に潰します。 一方 grep は、マッチが無いだけなら標準エラーに何も書きません。その有無が唯一の判定材料です。

変更

既定検出と extra_patterns の grep をどちらも既存の err_file に追記させ、両方が終わった時点で 1 回だけ判定します。

+    : > "$err_file"
     hits=$(
         cd "$repo_dir" || exit 1
         git ls-files -z \
-            | xargs -0 -r grep -EoHn "${image}:[A-Za-z0-9._-]+" 2>/dev/null \
+            | xargs -0 -r grep -EoHn "${image}:[A-Za-z0-9._-]+" 2>>"$err_file" \
             | sed "s|:${image}:|:|" || true
     )
...
+    if [ -s "$err_file" ]; then
+        errors=$((errors + 1))
+        log "  ERROR: ${name} — grep が失敗した: $(tr '\n' ' ' < "$err_file" | cut -c1-200)"
+        continue
+    fi

errors カウンタと ERROR: 行は clone 失敗が既に使っている枠組みで、そこに乗せています。

実測で検証しました

BSD grep(-P 非対応)を意図的に呼び、3 つのケースを確認しました。

① 修正前(2>/dev/null)
   hits の件数: 0
   → 「drift なし」と報告。実際には grep が起動していない

② 修正後(2>>$err_file + 判定)
   ERROR: latex-release-action — grep が失敗した: grep: invalid option -- P usage: ...
   → errors=1 として continue

③ マッチが無いだけのとき(正常系)
   stderr のサイズ: 0 バイト
   → 誤検出しない

③ が重要で、マッチなしを異常と誤判定しないことを確認しています。

設計思想は既にこのファイルにありました

同じスクリプトの cd にこう書かれています。

cd の失敗は握り潰さない。空の結果は「一致している」と区別が付かず、何も見ていない監査が ok を出すことになる

audit-repo-protection.sh にも同趣旨のコメントがあります。考え方は共有されていて、grep の呼び出しにだけ及んでいませんでした。

補足: #173 の当初の記述は誤りでした

この issue は当初「macOS で動かない(xargs -r が BSD にない)」として起票しましたが、xargs -r は macOS でも動きます。また PCRE 非対応環境はスクリプト冒頭のチェックが正しく弾きます(実際 macOS で実行すると grep が PCRE (-P) に対応していない。GNU grep が要る で停止します)。スクリプトのガードは設計どおり機能しており、残る問題は標準エラーの握り潰しだけでした。 issue 本文はその 1 点に絞って書き直してあります。

The audit discarded grep's standard error, so a grep that never ran
produced the same empty result as one that found nothing, and the
repository was reported as ok. A detector that answers "no drift" while
broken is the failure this audit exists to avoid, and the same file
already says so about cd:

  cd の失敗は握り潰さない。空の結果は「一致している」と区別が付かず、
  何も見ていない監査が ok を出すことになる

Exit codes cannot carry this: grep returns 1 when nothing matched, and
xargs collapses any child status in 1..125 to 123, so "matched nothing"
and "could not start" arrive as the same number. Standard error can --
grep writes nothing there when it simply found no match. Both greps now
append to the existing err_file and one check after them raises the
error, reusing the counter and ERROR line the clone failure already uses.

Refs #173

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

全体的に、grep の失敗を /dev/null で握り潰していた問題を正しく修正しており、設計思想も一貫しています。1点、実運用上の影響が大きい懸念を指摘します。

# 返すため、「一致しなかった」と「起動できなかった」が同じ値になる。
# grep はマッチが無いだけなら標準エラーに何も書かないので、その有無で
# 判定する。捨ててしまうと、検出器が動いていない監査が ok を出す
: > "$err_file"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [MEDIUM] : > "$err_file" でファイルをクリアしてから、extra_patterns のループ内で 2>>"$err_file" と追記しています。この設計では、ループの途中のイテレーションで grep が失敗しても、後続のイテレーションが成功すれば err_file にはエラーが残り続けるという点は問題ありません。しかし逆に、最初の hits= の grep が失敗した場合でも、extra_patterns ループが全て正常終了すれば err_file にはエラーが書き込まれたまま残るため、正しく検出されます。

ただし、より深刻な問題として、err_file複数のリポジトリ間で共有されている場合、前のリポジトリの grep エラーが残っていると誤検知が起きます。: > "$err_file" のクリアはループの先頭(各リポジトリの処理開始時)に置かれているため、この点は正しく対処されています。

一方で、err_file の定義箇所(スクリプト上部)を確認し、mktemp 等で一時ファイルとして作成されているか確認してください。もし固定パスであれば、並列実行時に競合が生じます。スクリプトが並列実行される可能性がある場合は mktemp による一時ファイルの利用を推奨します。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確認しました。mktemp で作成されています。 固定パスではないので並列実行での競合はありません。

workdir=$(mktemp -d)
err_file=$(mktemp)
trap 'rm -rf "$workdir" "$err_file"' EXIT

trap によるクリーンアップも入っています。ご指摘の推奨事項は既に満たされている形です。

前半で挙げていただいた 2 点についても、認識は一致しています。

  • 最初の hits= の grep が失敗し、後続の extra_patterns が正常終了しても、err_file の内容は残るので検出される
  • リポジトリ間の持ち越しは、各リポジトリの処理開始時に : > "$err_file" でクリアすることで防いでいる

なお : > "$err_file" を置いた位置には、もう 1 つ理由があります。clone が >"$err_file" 2>&1 で同じファイルに書くため、clone が成功しても何か出力していた場合にそれが残ります。grep の判定を始める前にクリアしないと、clone の出力を grep の失敗と誤認します。

コード変更は不要と判断しました。


if [ -s "$err_file" ]; then
errors=$((errors + 1))
log " ERROR: ${name} — grep が失敗した: $(tr '\n' ' ' < "$err_file" | cut -c1-200)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ [LOW] $(tr '\n' ' ' < "$err_file" | cut -c1-200) はコマンド置換内でファイルを読み込んでいますが、err_file に制御文字や特殊なバイト列が含まれる場合(悪意あるリポジトリ名やファイル名など)、ログ出力が意図しない形式になる可能性があります。実害は限定的ですが、cat "$err_file" | head -c 200 のように単純化するか、出力をクォートで保護することを検討してください。現状の log 関数がどのように実装されているかにも依存しますが、ログインジェクションのリスクとして認識しておくべきです。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

据え置きます。 理由は、この書き方が既存の ERROR 行と揃えたものだからです。

同じスクリプトの clone 失敗が、以前から同じ形を使っています。

log "  ERROR: ${name} — clone できなかった: $(tr '\n' ' ' < "$err_file")"

今回の行はこれに cut -c1-200 を足しただけです(grep のエラーは複数ファイル分が並んで長くなりうるため)。ここだけ制御文字を落とすと、同じファイル内の同種の 2 行で扱いが分かれます。 対処するならスクリプト全体、さらには audit-repo-protection.sh を含めた方針として決めるべきものです。

脅威モデルについても、実害は限定的だと考えています。

  • 監査対象は config/texlive-tag-refs.json に列挙した smkwlab org の 12 リポジトリで、ファイル名は自分たちが管理している
  • err_file に入るのは grep / xargs / gh のエラーメッセージで、任意の入力ではない
  • log()echo "[$(date ...)] $1" で、-e を付けていないためバックスラッシュ列は解釈されない

残るリスクは、追跡ファイル名に生の ANSI エスケープを含むリポジトリを監査対象に加えた場合に CI ログの表示が乱れることですが、この監査は org 内の自リポジトリを対象にしたもので、そこまでの入力を想定していません。

もし方針として制御文字を落とすなら、tr -d '\000-\010\013\014\016-\037' のように範囲を限る必要があります。[:print:] で絞ると UTF-8 の継続バイトが巻き込まれ、日本語のファイル名やエラーメッセージが壊れます。このリポジトリのログは日本語なので、ここは慎重に決める必要があります。

@toshi0806

Copy link
Copy Markdown
Member Author

レビュー対応

MEDIUM 1 件・LOW 1 件、いずれも確認のうえ据え置きました。コード変更はありません。CI は review / review / actionlint とも success です。

MEDIUM: err_file が固定パスだと並列実行で競合する(r3940863212)→ 既に mktemp

workdir=$(mktemp -d)
err_file=$(mktemp)
trap 'rm -rf "$workdir" "$err_file"' EXIT

推奨されている形になっており、クリーンアップの trap も入っています。指摘の前半(イテレーション間の残留とリポジトリ間の持ち越し)についても認識は一致しています。

補足すると、: > "$err_file" の位置にはもう 1 つ理由があります。clone が >"$err_file" 2>&1 で同じファイルを使うため、クリアしないと clone の出力を grep の失敗と誤認します。

LOW: ログインジェクションのリスク(r3940863215)→ 据え置き

この書き方は既存の ERROR 行に揃えたものです。 clone 失敗が以前から $(tr '\n' ' ' < "$err_file") を使っており、今回の行はそれに cut -c1-200 を足しただけです(grep のエラーは複数ファイル分で長くなりうるため)。ここだけ制御文字を落とすと、同じファイル内の同種の 2 行で扱いが分かれます。

脅威モデルとしても、監査対象は宣言に列挙した org 内 12 リポジトリでファイル名は自分たちが管理しており、err_file に入るのは grep / xargs / gh のエラーメッセージです。log()echo-e を付けていないのでバックスラッシュ列も解釈されません。

対処するならスクリプト全体、さらに audit-repo-protection.sh を含めた方針として決めるべきと考えます。その際は tr -d '\000-\010\013\014\016-\037' のように範囲を限る必要があります。[:print:] で絞ると UTF-8 の継続バイトを巻き込み、このリポジトリのログで使われている日本語が壊れます


全指摘に返信済み・CI green で収束しました。Resolves #173 を含むため、マージ時に #173 が自動クローズされます。

@toshi0806
toshi0806 merged commit a4fd0aa into main Sep 6, 2026
4 checks passed
@toshi0806
toshi0806 deleted the tell-grep-failures-from-zero-matches branch September 6, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

audit-texlive-tags.sh cannot tell a grep failure from zero matches

1 participant